在 Day11 的功能構想中我們有提到,雖然到昨天 Day19 整個貪吃蛇遊戲已經很完整了,但是有一點美中不足的地方,就是透過手機開啟這個遊戲的時候,會發現竟然沒有方向鍵
可以按!
過去在貪吃蛇流行的非智慧型手機年代,直接有方向鍵可以操作,但是現在手機已經進化到只剩下一個螢幕了,所以雖然一開始很高興的把遊戲做成可以適應手機螢幕大小,雖然適應是適應了,但是卻只能按下開始遊戲,然後就無法操作了,狀況非常的囧。而且目前時下大部分的人都使用手機來瀏覽網頁,所以如果過年回家之後,在小朋友面前炫耀自己做了貪吃蛇,卻發現沒有方向鍵可以玩,就會當場從天堂掉到地獄,非常的淒慘。
所以為了要避免這種尷尬的狀況,讓我們從地獄返回天堂,我們最後要做的一件事情就是希望在螢幕上手刻出一個虛擬的上下左右鍵,順便在今天也加上可以暫停遊戲的功能,方便我們玩到一半突然想上廁所的時候不會很煩惱。
首先,我們先新增一個命名為 <VirtualKeyboard />
的元件
containers/SnakeGame/components/VirtualKeyboard/index.js
import {
ARROW_UP,
ARROW_DOWN,
ARROW_LEFT,
ARROW_RIGHT,
} from 'containers/SnakeGame/constants';
const VirtualKeyboard = ({ handleOnClick }) => (
<StyledVirtualKeyboard>
<div>
<div data-code={ARROW_UP}>上</div>
</div>
<div className="virtual-keyboard__wrapper-bottom">
<div data-code={ARROW_LEFT}>左</div>
<div data-code={ARROW_DOWN}>下</div>
<div data-code={ARROW_RIGHT}>右</div>
</div>
</StyledVirtualKeyboard>
);
再來我們在元件裡面新增四個分別為上下左右的 div tag ,因為我們希望 ArrowUp 按鈕是同一個 row 層,另外 ArrowLeft, ArrowDown, ArrowRight 是同一 row 層,這兩層分層又分上下
。所以為了達成這樣的排列,我把同一層的按鈕用一個 div tag 包起來,方便我做排版,如上面程式碼。
下圖是上面程式碼的結果
上面示意圖當中我們給了文字的上下左右方便大家辨識,但是這邊我想要用箭頭圖案來代替文字,一方面比較美觀,另一方面比較直覺。圖案的部分我想直接使用 Font Awesome 上面的icon。
做了一些樣式調整之後,如下圖:
有了上下左右操作鍵的外觀之後,接下來要做他的功能。
記得我們之前在 Day15 做鍵盤操作上下左右功能的時候,我們拿到的 event.code 分別是 ArrowUp, ArrowLeft, ArrowDown, ArrowRight ,為了可以重複使用我們之前寫過的程式,我分別給每一個按鈕資料屬性(data-* attribute),上箭頭就給他 data-code={’ArrowUp’} ,下箭頭就給他 data-code={‘ArrowDown’} ,依此類推給他相對應的屬性。
再來我在每一個箭頭 onClick 的時候,透過傳入 <VirtualKeyboard />
的函數來取的相對應的資料屬性
<VirtualKeyboard handleOnClick={this.handleOnVirtualKeyboardClick} />
拿到資料屬性之後,就跟前面 Day15 透過鍵盤操作上下左右功能時一樣的方法,來改變 snake 的 direction 方向屬性
handleOnVirtualKeyboardClick = (event) => {
const {
handleOnSetSnakeDirection,
} = this.props;
const code = event.currentTarget.getAttribute('data-code');
handleOnSetSnakeDirection(code);
}
完成畫面上的方向鍵之後,最後一個步驟,我要來實作暫停按鈕。
為了讓遊戲可以判斷是否現在是暫停的狀態,還是繼續遊戲的狀態,我們要用一個布林值的參數,我命名為 isPause ,當 isPause 為 true 的時候,遊戲暫停,反之,遊戲繼續。
containers/SnakeGame/index.js
<div data-code={SPACE} onClick={this.handleOnVirtualKeyboardClick} <div
data-code={SPACE}
onClick={this.handleOnVirtualKeyboardClick}
className="snake-game__pause-game-btn"
>
{
isPause ? '繼續' : '暫停'
}
</div>
然後因為我希望除了畫面上有暫停按鈕,在鍵盤控制的時候,按下空白鍵
也可以切換暫停和繼續,就像是上下左右鍵操作一樣,所以我這邊也使用空白鍵的 event.code 來作為判斷,所以空白鍵的 event.code 是 Space
,我也把畫面上暫停按鈕的資料屬性設為跟 event.code 一樣為 data-code={‘Space’}。
有了可以切換暫停功能的參數之後,我們就可以透過 isPause 來控制遊戲是否暫停,如下程式碼,遊戲暫停的時候,我們會用 clearInterval() 來讓 setInterval() 停止調用內部函數,當切換成繼續遊戲的時候,再重新執行 setInterval()。
componentDidUpdate(prevProps, prevState) {
const {
isSpeedModified,
} = prevProps;
const {
snake,
isGameStart,
isPause,
handleOnSetSnakeMoving,
handleOnSetSpeedModified,
} = this.props;
if (isPause) {
clearInterval(gameInterval);
}
if (isSpeedModified) { // to udpate speed
handleOnSetSpeedModified(false);
clearInterval(gameInterval);
gameInterval = setInterval(() => {
if (isGameStart && !isPause) {
handleOnSetSnakeMoving()
}
}, snake.get('speed'));
}
if (!isGameStart) {
clearInterval(gameInterval);
handleOnSetSpeedModified(true);
}
}
另一部分,我們希望在暫停的時候,上下左右控制鍵會失去功能,避免在暫停期間去改變了蛇的運動方向,導致繼續遊戲的時候,出現錯誤
containers/SnakeGame/reducer.js
case SET_SNAKE_DIRECTION: {
if (!state.get('isGameStart')) {
return state;
}
let isPause = state.get('isPause');
let isSpeedModified = state.get('isSpeedModified');
if (action.payload === 'Space') {
isPause = !state.get('isPause');
isSpeedModified = isPause;
}
if (!direction[action.payload] && !(action.payload === 'Space')) {
return state;
}
return state.updateIn(['snake', 'direction'], (dir) => {
if (action.payload === 'Space' || isPause) {
return dir;
}
if (dir.get('x') * -1 === direction[action.payload].x &&
dir.get('y') * -1 === direction[action.payload].y) {
return dir;
}
return fromJS(direction[action.payload]);
})
.set('isPause', isPause)
.set('isSpeedModified', isSpeedModified);
}
到這邊,我們在 Day11 所希望做的所有貪吃蛇的功能就全部完成啦!下面是我們最終成果的演示: